//  MOEAD.java
//
//  Author:
//       Antonio J. Nebro <antonio@lcc.uma.es>
//       Juan J. Durillo <durillo@lcc.uma.es>
//
//  Copyright (c) 2011 Antonio J. Nebro, Juan J. Durillo
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

package etmo.metaheuristics.matbml.libs;

import etmo.core.Operator;
import etmo.core.ProblemSet;
import etmo.core.Solution;
import etmo.core.SolutionSet;
import etmo.metaheuristics.moead.Utils;
import etmo.util.JMException;
import etmo.util.PseudoRandom;
import etmo.util.logging.LogPopulation;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.util.Vector;

public class MOEAD extends MaTAlgorithm {

	private int populationSize_;

	private int H_;
	/**
	 * Stores the population
	 */
	private SolutionSet population_;
	/**
	 * Z vector (ideal point)
	 */
	double[] z_;
	/**
	 * Lambda vectors
	 */
	// Vector<Vector<Double>> lambda_ ;
	double[][] lambda_;
	/**
	 * T: neighbour size
	 */
	int T_;
	/**
	 * Neighborhood
	 */
	int[][] neighborhood_;
	/**
	 * delta: probability that parent solutions are selected from neighbourhood
	 */
	double delta_;
	/**
	 * nr: maximal number of solutions replaced by each child solution
	 */
	int nr_;
	Solution[] indArray_;
	String functionType_;
	int evaluations_;
	int maxEvaluations_;

	boolean initialized = false;
	/**
	 * Operators
	 */
	Operator crossover_;
	Operator mutation_;

	String dataDirectory_;

	/**
	 * Constructor
	 *
	 * @param problemSet
	 *            Problem to solve
	 */
	public MOEAD(ProblemSet problemSet) throws JMException, ClassNotFoundException {
		super(problemSet);

		functionType_ = "_TCHE1";
	} // MOEAD

	public MOEAD(ProblemSet problemSet, SolutionSet solutionSet, int taskIdx) throws JMException, ClassNotFoundException {
		super(problemSet);
		population_ = solutionSet;
		populationSize_ = solutionSet.size();
		taskIdx_ = taskIdx;

		functionType_ = "_TCHE1";
	}

	public SolutionSet execute() throws JMException, ClassNotFoundException {
		maxEvaluations_ = ((Integer) this.getInputParameter("maxEvaluations")).intValue();
		if (population_ == null) {
			populationSize_ = ((Integer) this.getInputParameter("populationSize")).intValue();
			population_ = new SolutionSet(populationSize_);
		}

		dataDirectory_ = this.getInputParameter("dataDirectory").toString();
		//System.out.println("POPSIZE: " + populationSize_);

		indArray_ = new Solution[problemSet_.get(0).getNumberOfObjectives()];

		T_ = ((Integer) this.getInputParameter("T")).intValue();
		nr_ = ((Integer) this.getInputParameter("nr")).intValue();
		delta_ = ((Double) this.getInputParameter("delta")).doubleValue();

		/*
		 * T_ = (int) (0.1 * populationSize_); delta_ = 0.9; nr_ = (int) (0.01 *
		 * populationSize_);
		 */
		neighborhood_ = new int[populationSize_][T_];

		z_ = new double[problemSet_.get(0).getNumberOfObjectives()];
		// lambda_ = new Vector(problemSet_.get(0).getNumberOfObjectives()) ;
		lambda_ = new double[populationSize_][problemSet_.get(0).getNumberOfObjectives()];

		crossover_ = operators_.get("crossover"); // default: DE crossover
		mutation_ = operators_.get("mutation"); // default: polynomial mutation

		H_= 0;

		while ((H_+3) * (H_+2) / 2 < populationSize_)
			H_++;

		// STEP 1. Initialization
		// STEP 1.1. Compute euclidean distances between weight vectors and find
		// T
		initUniformWeight();
		// for (int i = 0; i < 300; i++)
		// System.out.println(lambda_[i][0] + " " + lambda_[i][1]) ;

		initNeighborhood();

//		// STEP 1.2. Initialize population
		initPopulation();
//
//		// STEP 1.3. Initialize z_
		initIdealPoint();
		// STEP 2. Update
		do {
			int[] permutation = new int[populationSize_];
			Utils.randomPermutation(permutation, populationSize_);

			for (int i = 0; i < populationSize_; i++) {
				int n = permutation[i]; // or int n = i;
				// int n = i ; // or int n = i;
				int type;
				double rnd = PseudoRandom.randDouble();

				// STEP 2.1. Mating selection based on probability
				if (rnd < delta_) // if (rnd < realb)
				{
					type = 1; // neighborhood
				} else {
					type = 2; // whole population
				}
				Vector<Integer> p = new Vector<Integer>();
				matingSelection(p, n, 2, type);

				// STEP 2.2. Reproduction
				Solution child;
				Solution[] parents = new Solution[3];

				parents[0] = population_.get(p.get(0));
				parents[1] = population_.get(p.get(1));
				parents[2] = population_.get(n);

				// Apply DE crossover
				child = (Solution) crossover_.execute(new Object[] {
						population_.get(n), parents });

				/*Solution[] parents = new Solution[2];

				parents[0] = population_.get(p.get(0));
				parents[1] = population_.get(n);
				// Apply SBX crossover
				child = (Solution) crossover_.execute(parents);*/

				// Apply mutation
				mutation_.execute(child);

				int flag = PseudoRandom.randDouble() < 0.5 ? parents[0].getFlag() : parents[1].getFlag();
				child.setFlag(flag);

				// Evaluation
				problemSet_.get(0).evaluate(child);

				evaluations_++;

				// STEP 2.3. Repair. Not necessary

				// STEP 2.4. Update z_
				updateReference(child);

				// STEP 2.5. Update of solutions
				updateProblem(child, n, type);

				if (evaluations_ % (populationSize_ * 20) == 0){
					LogPopulation.LogPopulation("MOEAD", population_, problemSet_, evaluations_, false);
				}
			} // for
		} while (evaluations_ < maxEvaluations_);

		return population_;
	}

	public boolean step() throws JMException, ClassNotFoundException {
		int evaluations = 0;
		int maxEvaluations = populationSize_;
		while (evaluations_ < maxEvaluations_ && evaluations < maxEvaluations){
			int[] permutation = new int[populationSize_];
			Utils.randomPermutation(permutation, populationSize_);

			for (int i = 0; i < populationSize_; i++) {
				int n = permutation[i]; // or int n = i;
				// int n = i ; // or int n = i;
				int type;
				double rnd = PseudoRandom.randDouble();

				// STEP 2.1. Mating selection based on probability
				if (rnd < delta_) // if (rnd < realb)
				{
					type = 1; // neighborhood
				} else {
					type = 2; // whole population
				}
				Vector<Integer> p = new Vector<Integer>();
				matingSelection(p, n, 2, type);

				// STEP 2.2. Reproduction
				Solution child;
				Solution[] parents = new Solution[3];

				parents[0] = population_.get(p.get(0));
				parents[1] = population_.get(p.get(1));
				parents[2] = population_.get(n);

				// Apply DE crossover
				child = (Solution) crossover_.execute(new Object[]{
						population_.get(n), parents});

				/*Solution[] parents = new Solution[2];

				parents[0] = population_.get(p.get(0));
				parents[1] = population_.get(n);
				// Apply SBX crossover
				child = (Solution) crossover_.execute(parents);*/

				int flag = PseudoRandom.randDouble() < 0.5 ? parents[0].getFlag() : parents[1].getFlag();
				child.setFlag(flag);

				// Apply mutation
				mutation_.execute(child);

				// Evaluation
				problemSet_.get(taskIdx_).evaluate(child);

				evaluations_++;
				evaluations++;

				updateReference(child);

				// STEP 2.5. Update of solutions
				updateProblem(child, n, type);
			}
		}
		return evaluations_ < maxEvaluations_;
	}

	/**
	 * initUniformWeight
	 */
	/*public void initUniformWeight() {
		if ((problemSet_.get(0).getNumberOfObjectives() == 2) && (populationSize_ <= 300)) {
			for (int n = 0; n < populationSize_; n++) {
				double a = 1.0 * n / (populationSize_ - 1);
				lambda_[n][0] = a;
				lambda_[n][1] = 1 - a;
			} // for
		} // if
		else {
			String dataFileName;
			//dataFileName = "W" + problemSet_.get(0).getNumberOfObjectives() + "D_"
					//+ populationSize_ + ".dat";

			try {
				// Open the file
				//FileInputStream fis = new FileInputStream(dataDirectory_ + "/"
				//		+ dataFileName);
				FileInputStream fis = new FileInputStream(dataDirectory_);
				InputStreamReader isr = new InputStreamReader(fis);
				BufferedReader br = new BufferedReader(isr);

				int numberOfObjectives = 0;
				int i = 0;
				int j = 0;
				String aux = br.readLine();
				while (aux != null) {
					StringTokenizer st = new StringTokenizer(aux);
					j = 0;
					numberOfObjectives = st.countTokens();
					while (st.hasMoreTokens()) {
						double value = (new Double(st.nextToken()))
								.doubleValue();
						lambda_[i][j] = value;
						// System.out.println("lambda["+i+","+j+"] = " + value)
						// ;
						j++;
					}
					aux = br.readLine();
					i++;
				}
				br.close();
			} catch (Exception e) {
				//System.out
				//		.println("initUniformWeight: failed when reading for file: "
				//				+ dataDirectory_ + "/" + dataFileName);
				System.out
						.println("initUniformWeight: failed when reading for file: "
								+ dataDirectory_);
				e.printStackTrace();
			}
		} // else

		// System.exit(0) ;
	} // initUniformWeight*/

	public void initState() throws JMException, ClassNotFoundException {
		maxEvaluations_ = ((Integer) this.getInputParameter("maxEvaluations")).intValue();
		if (population_ == null) {
			population_ = new SolutionSet(populationSize_);
			populationSize_ = ((Integer) this.getInputParameter("populationSize")).intValue();
		}
		evaluations_ = populationSize_;

		dataDirectory_ = this.getInputParameter("dataDirectory").toString();
		//System.out.println("POPSIZE: " + populationSize_);

		indArray_ = new Solution[problemSet_.get(taskIdx_).getNumberOfObjectives()];

		T_ = ((Integer) this.getInputParameter("T")).intValue();
		nr_ = ((Integer) this.getInputParameter("nr")).intValue();
		delta_ = ((Double) this.getInputParameter("delta")).doubleValue();

		/*
		 * T_ = (int) (0.1 * populationSize_); delta_ = 0.9; nr_ = (int) (0.01 *
		 * populationSize_);
		 */
		neighborhood_ = new int[populationSize_][T_];

		z_ = new double[problemSet_.get(taskIdx_).getNumberOfObjectives()];
		// lambda_ = new Vector(problemSet_.get(0).getNumberOfObjectives()) ;
		lambda_ = new double[populationSize_][problemSet_.get(taskIdx_).getNumberOfObjectives()];

		crossover_ = operators_.get("crossover"); // default: DE crossover
		mutation_ = operators_.get("mutation"); // default: polynomial mutation

		H_= 0;

		while ((H_+3) * (H_+2) / 2 < populationSize_)
			H_++;

		// STEP 1. Initialization
		// STEP 1.1. Compute euclidean distances between weight vectors and find
		// T
		initUniformWeight();
		// for (int i = 0; i < 300; i++)
		// System.out.println(lambda_[i][0] + " " + lambda_[i][1]) ;

		initNeighborhood();

//		// STEP 1.2. Initialize population
//		initPopulation();
//
		// STEP 1.3. Initialize z_
		initIdealPoint();

		initialized = true;
	}

	void initUniformWeight() { // init lambda vectors
		int nw = 0;
		if (problemSet_.get(taskIdx_).getNumberOfObjectives() == 2) {
			for (int n = 0; n < populationSize_; n++) {
				double a = 1.0 * n / (populationSize_ - 1);
				lambda_[n][0] = a;
				lambda_[n][1] = 1 - a;
				nw++;
			} // for
		} // if
		else if(problemSet_.get(taskIdx_).getNumberOfObjectives() == 3) {
			int i, j;
			for (i = 0; i <= H_; i++) {
				for (j = 0; j <= H_ - i; j++) {
					lambda_[nw][0] = (1.0 * i) / H_;
					lambda_[nw][1] = (1.0 * j) / H_;
					lambda_[nw][2] = 1.0 * (H_ - i - j) / H_;
					nw++;
				} // for
			} // for
			/*if (nw != populationSize_) {
			System.out.println(nw + "---" + (populationSize_));
			System.out.println("ERROR: population size <> #weights");
			System.exit(0);
			
		}*/
		}
		else{
			String dataFileName;
			dataFileName = "W" + problemSet_.get(taskIdx_).getNumberOfObjectives() + "D_"
					+ populationSize_ + ".dat";

			try {
				// Open the file
				FileInputStream fis = new FileInputStream(dataDirectory_ + "/"
						+ dataFileName);
				InputStreamReader isr = new InputStreamReader(fis);
				BufferedReader br = new BufferedReader(isr);

				int i = 0;
				int j = 0;
				String aux = br.readLine();
				while (aux != null) {
					StringTokenizer st = new StringTokenizer(aux);
					j = 0;
					while (st.hasMoreTokens()) {
						double value = Double.parseDouble(st.nextToken());
						lambda_[i][j] = value;
						j++;
					}
					aux = br.readLine();
					i++;
				}
				br.close();
			} catch (Exception e) {
				System.out
						.println("initUniformWeight: failed when reading for file: "
								+ dataDirectory_ + "/" + dataFileName);
				e.printStackTrace();
			}
		} // else


	} // initUniformWeight

	/**
   *
   */
	void initNeighborhood() {
		double[] x = new double[populationSize_];
		int[] idx = new int[populationSize_];

		for (int i = 0; i < populationSize_; i++) {
			// calculate the distances based on weight vectors
			for (int j = 0; j < populationSize_; j++) {
				x[j] = Utils.distVector(lambda_[i], lambda_[j]);
				// x[j] = dist_vector(population[i].namda,population[j].namda);
				idx[j] = j;
				// System.out.println("x["+j+"]: "+x[j]+
				// ". idx["+j+"]: "+idx[j]) ;
			} // for

			// find 'niche' nearest neighboring subproblems
			Utils.minFastSort(x, idx, populationSize_, T_);
			// minfastsort(x,idx,population.size(),niche);

			System.arraycopy(idx, 0, neighborhood_[i], 0, T_);
		} // for
	} // initNeighborhood

	/**
   *
   */
	void initPopulation() throws JMException, ClassNotFoundException {
		for (int i = 0; i < populationSize_; i++) {
			Solution newSolution = new Solution(problemSet_);

			problemSet_.get(taskIdx_).evaluate(newSolution);
			evaluations_++;
			population_.add(newSolution);
		} // for
	} // initPopulation

	/**
   *
   */
	void initIdealPoint() throws JMException, ClassNotFoundException {
		for (int i = 0; i < problemSet_.get(taskIdx_).getNumberOfObjectives(); i++) {
			z_[i] = 1.0e+30;
			indArray_[i] = new Solution(problemSet_);
			problemSet_.get(taskIdx_).evaluate(indArray_[i]);
			evaluations_++;
		} // for

		for (int i = 0; i < populationSize_; i++) {
			updateReference(population_.get(i));
		} // for
	} // initIdealPoint

	/**
   *
   */
	public void matingSelection(Vector<Integer> list, int cid, int size,
			int type) {
		// list : the set of the indexes of selected mating parents
		// cid : the id of current subproblem
		// size : the number of selected mating parents
		// type : 1 - neighborhood; otherwise - whole population
		int ss;
		int r;
		int p;

		ss = neighborhood_[cid].length;
		while (list.size() < size) {
			if (type == 1) {
				r = PseudoRandom.randInt(0, ss - 1);
				p = neighborhood_[cid][r];
				// p = population[cid].table[r];
			} else {
				p = PseudoRandom.randInt(0, populationSize_ - 1);
			}
			boolean flag = true;
			for (int i = 0; i < list.size(); i++) {
				if (list.get(i) == p) // p is in the list
				{
					flag = false;
					break;
				}
			}

			// if (flag) list.push_back(p);
			if (flag) {
				list.addElement(p);
			}
		}
	} // matingSelection

	/**
	 *
	 * @param individual
	 */
	void updateReference(Solution individual) {
		int objStart = problemSet_.get(taskIdx_).getStartObjPos();
		for (int n = 0; n < problemSet_.get(taskIdx_).getNumberOfObjectives(); n++) {
			if (individual.getObjective(n + objStart) < z_[n]) {
				z_[n] = individual.getObjective(n + objStart);

				indArray_[n] = individual;
			}
		}
	} // updateReference

	/**
	 * @param indiv
	 * @param id
	 * @param type
	 */
	void updateProblem(Solution indiv, int id, int type) {
		// indiv: child solution
		// id: the id of current subproblem
		// type: update solutions in - neighborhood (1) or whole population
		// (otherwise)
		int size;
		int time;

		time = 0;

		if (type == 1) {
			size = neighborhood_[id].length;
		} else {
			size = population_.size();
		}
		int[] perm = new int[size];

		Utils.randomPermutation(perm, size);

		for (int i = 0; i < size; i++) {
			int k;
			if (type == 1) {
				k = neighborhood_[id][perm[i]];
			} else {
				k = perm[i]; // calculate the values of objective function
								// regarding the current subproblem
			}
			double f1, f2;

			f1 = fitnessFunction(population_.get(k), lambda_[k]);
			f2 = fitnessFunction(indiv, lambda_[k]);

			if (f2 < f1) {
				population_.replace(k, new Solution(indiv));
				// population[k].indiv = indiv;
				time++;
			}
			// the maximal number of solutions updated is not allowed to exceed
			// 'limit'
			if (time >= nr_) {
				return;
			}
		}
	} // updateProblem

	double fitnessFunction(Solution individual, double[] lambda) {
		double fitness;
		fitness = 0.0;

		if (functionType_.equals("_TCHE1")) {
			double maxFun = -1.0e+30;

			for (int n = 0; n < problemSet_.get(taskIdx_).getNumberOfObjectives(); n++) {
				double diff = Math.abs(
						individual.getObjective(n + problemSet_.get(taskIdx_).getStartObjPos()) - z_[n]);

				double feval;
				if (lambda[n] == 0) {
					feval = 0.0001 * diff;
				} else {
					feval = diff * lambda[n];
				}
				if (feval > maxFun) {
					maxFun = feval;
				}
			} // for

			fitness = maxFun;
		} // if
		else {
			System.out.println("MOEAD.fitnessFunction: unknown type "
					+ functionType_);
			System.exit(-1);
		}
		return fitness;
	} // fitnessEvaluation
} // MOEAD

